package org.decathlon.fwk.restExceptionHandler;

import org.apache.log4j.Logger;
import org.springframework.beans.TypeMismatchException;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.context.MessageSource;
import org.springframework.context.MessageSourceAware;
import org.springframework.http.HttpStatus;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;
import org.springframework.web.HttpMediaTypeNotAcceptableException;
import org.springframework.web.HttpMediaTypeNotSupportedException;
import org.springframework.web.HttpRequestMethodNotSupportedException;
import org.springframework.web.bind.MissingServletRequestParameterException;
import org.springframework.web.context.request.ServletWebRequest;
import org.springframework.web.servlet.mvc.multiaction.NoSuchRequestHandlingMethodException;

import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.Locale;
import java.util.Map;

public class RestErrorResolver implements InitializingBean,MessageSourceAware,IRestExceptionResolver {

    public static final String DEFAULT_EXCEPTION_MESSAGE_VALUE = "_exmsg";
    public static final String DEFAULT_MESSAGE_VALUE = "_msg";

    private static final Logger log = Logger.getLogger(RestExceptionHandler.class);

    private Map<String,RestError> exceptionMappings= Collections.emptyMap();
    private Map<String,String> exceptionMappingDefinitions = Collections.emptyMap();


    private MessageSource messageSource;

    private String defaultMoreInfoUrl;
    private boolean defaultEmptyCodeToStatus;
    private String defaultDeveloperMessage;

    public RestErrorResolver() {
        this.defaultEmptyCodeToStatus = true;
        this.defaultDeveloperMessage = DEFAULT_EXCEPTION_MESSAGE_VALUE;
    }


    public Map<String, RestError> getExceptionMappings() {
        return exceptionMappings;
    }

    public void setExceptionMappings(Map<String, RestError> exceptionMappings) {
        this.exceptionMappings = exceptionMappings;
    }

    public Map<String, String> getExceptionMappingDefinitions() {
        return exceptionMappingDefinitions;
    }

    public void setExceptionMappingDefinitions(Map<String, String> exceptionMappingDefinitions) {
        this.exceptionMappingDefinitions = exceptionMappingDefinitions;
    }

    public MessageSource getMessageSource() {
        return messageSource;
    }

    public String getDefaultMoreInfoUrl() {
        return defaultMoreInfoUrl;
    }

    public void setDefaultMoreInfoUrl(String defaultMoreInfoUrl) {
        this.defaultMoreInfoUrl = defaultMoreInfoUrl;
    }

    public boolean isDefaultEmptyCodeToStatus() {
        return defaultEmptyCodeToStatus;
    }

    public void setDefaultEmptyCodeToStatus(boolean defaultEmptyCodeToStatus) {
        this.defaultEmptyCodeToStatus = defaultEmptyCodeToStatus;
    }

    public String getDefaultDeveloperMessage() {
        return defaultDeveloperMessage;
    }

    public void setDefaultDeveloperMessage(String defaultDeveloperMessage) {
        this.defaultDeveloperMessage = defaultDeveloperMessage;
    }

    public void afterPropertiesSet() throws Exception {
        //populate with some defaults:
        Map<String, String> definitions = createDefaultExceptionMappingDefinitions();

        //add in user-specified mappings (will override defaults as necessary):
        if (this.exceptionMappingDefinitions != null && !this.exceptionMappingDefinitions.isEmpty()) {
            definitions.putAll(this.exceptionMappingDefinitions);
        }

        this.exceptionMappings = toRestErrors(definitions);
    }


    public RestError resolveError(ServletWebRequest request, Object handler, Exception ex) {
        if(ex==null){
            log.info("no exception to resolve");
            return null;
        }

        RestError template = getRestErrorTemplate(ex);

        if(template==null){
            log.info("Cannot resolve the error template for exception :"+ex.getClass().getName());
            return null;
        }

        RestError.Builder builder = new RestError.Builder();
        builder.setCode(template.getCode());
        builder.setMoreInfoUrl(template.getMoreInfoUrl()==null?this.defaultMoreInfoUrl:template.getMoreInfoUrl());
        builder.setStatus(template.getStatus());
        builder.setThrowable(ex);

        String message = getMessage(template,request,ex);
        builder.setMessage(message);
        builder.setDeveloperMessage(getDeveloperMessage(template,request,ex));

        return builder.build();
    }
    protected String getDeveloperMessage(RestError template, ServletWebRequest request, Exception ex) {
        String devMsg = template.getDeveloperMessage();
        if (devMsg == null && defaultDeveloperMessage != null) {
            devMsg = defaultDeveloperMessage;
        }
        if (DEFAULT_MESSAGE_VALUE.equals(devMsg)) {
            devMsg = template.getMessage();
        }
        return getMessage(devMsg, request, ex);
    }

    private String getMessage(String msg, ServletWebRequest request, Exception ex) {
        if (msg != null) {
            if (msg.equalsIgnoreCase("null") || msg.equalsIgnoreCase("off")) {
                return null;
            }
            if (msg.equalsIgnoreCase(DEFAULT_EXCEPTION_MESSAGE_VALUE)) {
                msg = ex.getMessage();
            }
            if (messageSource != null) {
                Locale locale = Locale.ENGLISH;
                msg = messageSource.getMessage(msg, null, msg, locale);
            }
        }

        return msg;

    }

    private String getMessage(RestError template, ServletWebRequest request, Exception ex) {
        String msg = template.getMessage();
        return getMessage(msg, request, ex);
    }

    private RestError getRestErrorTemplate(Exception ex){
        //find the matched exception class or the parent exception class

        Map<String,RestError> exceptionMap = this.exceptionMappings;
        RestError restError=null;

        if(CollectionUtils.isEmpty(exceptionMap)){
            return null;
        }else{
            String matchedClass="";
            for(Map.Entry<String,RestError> entry : exceptionMap.entrySet()){
                String key = entry.getKey();
                matchedClass = getMatchedClass(key,ex.getClass());
                if(StringUtils.isEmpty(matchedClass)){
                    continue;
                }else{
                    restError=entry.getValue();
                    break;
                }
            }
            if(log.isDebugEnabled()){
                log.debug("Resolving to RestError template '" + restError + "' for exception of type [" + ex.getClass().getName() +
                        "], based on exception mapping [" + matchedClass + "]");
            }
            return restError;
        }
    }

    private String getMatchedClass(String key, Class classToMath){
        if(key.equals(classToMath.getName())){
            return classToMath.getName();
        }
        if(classToMath.equals(Throwable.class)){
            return null;
        }
        else{
            return getMatchedClass(key,classToMath.getSuperclass());
        }
    }

    public void setMessageSource(MessageSource messageSource) {
        this.messageSource=messageSource;
    }

    private final Map<String,RestError> toRestErrors(Map<String, String> definitions) {
        if(definitions==null||definitions.size()==0){
            return Collections.emptyMap();
        }

        Map<String,RestError> map = new LinkedHashMap<String,RestError>(definitions.size());

        for(Map.Entry<String,String> entry : definitions.entrySet()){
            String key = entry.getKey();
            String value = entry.getValue();
            RestError restError = toRestError(value);
            map.put(key,restError);
        }

        return map;
    }

    private final RestError toRestError(String exceptionConfig) {

        String[] values = StringUtils.commaDelimitedListToStringArray(exceptionConfig);
        if (values == null || values.length == 0) {
            throw new IllegalStateException("Invalid config mapping.  Exception names must map to a string configuration.");
        }

        RestError.Builder builder = new RestError.Builder();

        boolean statusSet = false;
        boolean codeSet = false;
        boolean msgSet = false;
        boolean devMsgSet = false;
        boolean moreInfoSet = false;

        for (String value : values) {

            String trimmedVal = StringUtils.trimWhitespace(value);

            //check to see if the value is an explicitly named key/value pair:
            String[] pair = StringUtils.split(trimmedVal, "=");
            if (pair != null) {
                //explicit attribute set:
                String pairKey = StringUtils.trimWhitespace(pair[0]);
                if (!StringUtils.hasText(pairKey)) {
                    pairKey = null;
                }
                String pairValue = StringUtils.trimWhitespace(pair[1]);
                if (!StringUtils.hasText(pairValue)) {
                    pairValue = null;
                }
                if ("status".equalsIgnoreCase(pairKey)) {
                    int statusCode = getRequiredInt(pairKey, pairValue);
                    builder.setStatus(statusCode);
                    statusSet = true;
                } else if ("code".equalsIgnoreCase(pairKey)) {
                    int code = getRequiredInt(pairKey, pairValue);
                    builder.setCode(code);
                    codeSet = true;
                } else if ("msg".equalsIgnoreCase(pairKey)) {
                    builder.setMessage(pairValue);
                    msgSet = true;
                } else if ("devMsg".equalsIgnoreCase(pairKey)) {
                    builder.setDeveloperMessage(pairValue);
                    devMsgSet = true;
                } else if ("infoUrl".equalsIgnoreCase(pairKey)) {
                    builder.setMoreInfoUrl(pairValue);
                    moreInfoSet = true;
                }
            } else {
                //not a key/value pair - use heuristics to determine what value is being set:
                int val;
                if (!statusSet) {
                    val = getInt("status", trimmedVal);
                    if (val > 0) {
                        builder.setStatus(val);
                        statusSet = true;
                        continue;
                    }
                }
                if (!codeSet) {
                    val = getInt("code", trimmedVal);
                    if (val > 0) {
                        builder.setCode(val);
                        codeSet = true;
                        continue;
                    }
                }
                if (!moreInfoSet && trimmedVal.toLowerCase().startsWith("http")) {
                    builder.setMoreInfoUrl(trimmedVal);
                    moreInfoSet = true;
                    continue;
                }
                if (!msgSet) {
                    builder.setMessage(trimmedVal);
                    msgSet = true;
                    continue;
                }
                if (!devMsgSet) {
                    builder.setDeveloperMessage(trimmedVal);
                    devMsgSet = true;
                    continue;
                }
            }
        }

        return builder.build();

    }

    private final int getRequiredInt(String key, String value) {
        try {
            int anInt = Integer.valueOf(value);
            return Math.max(-1, anInt);
        } catch (NumberFormatException e) {
            String msg = "Configuration element '" + key + "' requires an integer value.  The value " +
                    "specified: " + value;
            throw new IllegalArgumentException(msg, e);
        }
    }

    private final int getInt(String key, String value) {
        try {
            return getRequiredInt(key, value);
        } catch (IllegalArgumentException iae) {
            return 0;
        }
    }

    protected final Map<String,String> createDefaultExceptionMappingDefinitions() {

        Map<String,String> m = new LinkedHashMap<String, String>();

        // 400
        applyDef(m, HttpMessageNotReadableException.class, HttpStatus.BAD_REQUEST);
        applyDef(m, MissingServletRequestParameterException.class, HttpStatus.BAD_REQUEST);
        applyDef(m, TypeMismatchException.class, HttpStatus.BAD_REQUEST);
        applyDef(m, "javax.validation.ValidationException", HttpStatus.BAD_REQUEST);

        // 404
        applyDef(m, NoSuchRequestHandlingMethodException.class, HttpStatus.NOT_FOUND);
        applyDef(m, "org.hibernate.ObjectNotFoundException", HttpStatus.NOT_FOUND);

        // 405
        applyDef(m, HttpRequestMethodNotSupportedException.class, HttpStatus.METHOD_NOT_ALLOWED);

        // 406
        applyDef(m, HttpMediaTypeNotAcceptableException.class, HttpStatus.NOT_ACCEPTABLE);

        // 409
        //can't use the class directly here as it may not be an available dependency:
        applyDef(m, "org.springframework.dao.DataIntegrityViolationException", HttpStatus.CONFLICT);

        // 415
        applyDef(m, HttpMediaTypeNotSupportedException.class, HttpStatus.UNSUPPORTED_MEDIA_TYPE);

        return m;
    }

    protected final void applyDef(Map<String,String> map, Class<?> exception, HttpStatus httpStatus){
        applyDef(map,exception.getName(),httpStatus);
    }

    protected final void applyDef(Map<String,String> map, String type, HttpStatus httpStatus){
        map.put(type,definitionFor(httpStatus));
    }

    private String definitionFor(HttpStatus status) {
        return status.value() + ", " + DEFAULT_EXCEPTION_MESSAGE_VALUE;
    }


}